#include "soundprison.h"
#include "ui_soundprison.h"

#include <QCloseEvent>
#include <QCryptographicHash>
#include <QDateTime>
#include <QDebug>
#include <QMenu>
#include <QMessageBox>
#include <QRadioButton>
#include <QSettings>
//#include <QScopedPointer>

#include <QxtGlobalShortcut>

#include "devicewidget.h"

#include <FunctionDiscoveryKeys_devpkey.h>

SoundPrison::SoundPrison(QWidget *parent)
	: QMainWindow(parent),
	  ui(new Ui::SoundPrison),
	  default_input_device(0),
	  default_output_device(0),
	  device_collection(NULL),
	  device_enum(NULL),
	  device_factory(new DeviceFactory(this)),
	  edit_mode(false),
	  settings(new QSettings(this)),
	  sys_tray(new QSystemTrayIcon(QIcon(":/soundprison.png"), this))
	{
		ui->setupUi(this);
		ui->preset_edit_widget->hide();

		connect(ui->button_box, SIGNAL(helpRequested()),
				this, SLOT(help()));
		connect(ui->button_box, SIGNAL(rejected()),
				qApp, SLOT(closeAllWindows()));

		connect(ui->apply_preset_btn, SIGNAL(clicked()),
				this, SLOT(applyPreset()));

		connect(ui->delete_preset_btn, SIGNAL(clicked()),
				this, SLOT(deletePreset()));

		connect(ui->edit_preset_btn, SIGNAL(clicked(bool)),
				this, SLOT(loadPreset(bool)), Qt::QueuedConnection);
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				this, SLOT(setEditMode(bool)));
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				ui->presets_list, SLOT(setDisabled(bool)));
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				ui->new_preset_btn, SLOT(setDisabled(bool)));
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				ui->apply_preset_btn, SLOT(setDisabled(bool)));
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				ui->delete_preset_btn, SLOT(setDisabled(bool)));
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				ui->preset_edit_widget, SLOT(setVisible(bool)));
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				ui->preset_name_edit, SLOT(setEnabled(bool)));
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				ui->preset_name_edit, SLOT(setFocus()));
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				ui->preset_shortcut_edit, SLOT(setEnabled(bool)));
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				ui->save_preset_btn, SLOT(setEnabled(bool)));
		connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
				ui->apply_startup_box, SLOT(setEnabled(bool)));

		connect(ui->new_preset_btn, SIGNAL(clicked()),
				this, SLOT(newPreset()));

		connect(ui->presets_list, SIGNAL(currentIndexChanged(QString)),
				ui->preset_name_edit, SLOT(setText(QString)));

		connect(ui->save_preset_btn, SIGNAL(clicked()),
				this, SLOT(savePreset()));

		connect(device_factory, SIGNAL(defaultDeviceChanged(QString,QString)),
				this, SLOT(updateDefaultDevice(QString,QString)), Qt::QueuedConnection);

		init();
		loadPresets();

		QMenu * menu = new QMenu(this);
		menu->addAction(tr("&Settings"));
		menu->addAction(tr("&Quit"), qApp, SLOT(quit()));
		sys_tray->setContextMenu(menu);
		sys_tray->show();

		connect(sys_tray, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
				this, SLOT(systrayActivated(QSystemTrayIcon::ActivationReason)));
	}

SoundPrison::~SoundPrison()
	{
		delete ui;
	}

void SoundPrison::addDevice(const QString &)
{
	//
}

void SoundPrison::applyPreset()
	{
		QString group = ui->presets_list->itemData(ui->presets_list->currentIndex()).toString();

		settings->beginReadArray(group);
			for (int i = 0; i < devices.size(); ++i)
			{
				settings->setArrayIndex(i);
				QStringList list = settings->value("device").toStringList();
				if (!list.count())
					break;

				foreach (DeviceWidget * dev_w, devices)
				{
					if (dev_w->deviceName() != list[0])
						continue;

					dev_w->setLocked(false);

					dev_w->setVolume(list[2].toInt());
					dev_w->mute((bool)list[3].toInt());

					if (!dev_w->radioBtn()->isChecked()
					 && (bool)list[4].toInt())
						dev_w->setAsDefaultDevice();

					dev_w->setLocked((bool)list[1].toInt());
				}
			}
		settings->endArray();

		settings->beginGroup(group);
			ui->lock_default_device_box->setChecked(settings->value("output_devices_locked").toBool());
			ui->lock_default_device_box_2->setChecked(settings->value("input_devices_locked").toBool());
		settings->endGroup();
	}

void SoundPrison::applyPresetByShortcut()
	{
		QObject * sender = this->sender();
		QxtGlobalShortcut * shortcut = qobject_cast<QxtGlobalShortcut *>(sender);
		QString key = global_shortcuts.key(shortcut);

		if (!key.isEmpty())
		{
			int index = ui->presets_list->findData(key);
			ui->presets_list->setCurrentIndex(index);
			applyPreset();
		}
	}

void SoundPrison::closeEvent(QCloseEvent * event)
	{
		static bool x;
		if (!x)
		{
			sys_tray->showMessage(tr("Sound Prison"),
								  tr("The program has only been hidden. "
									 "Click this icon to make Sound Prison visible. "
									 "Right click the icon for more options. "));
			x = true;
		}

		hide();
		event->ignore();
	}

void SoundPrison::deletePreset()
	{
		QString group = ui->presets_list->itemData(ui->presets_list->currentIndex()).toString();
		ui->presets_list->removeItem(ui->presets_list->currentIndex());
		settings->remove(group);

		bool x = ui->presets_list->count();
		ui->apply_preset_btn->setEnabled(x);
		ui->delete_preset_btn->setEnabled(x);
		ui->edit_preset_btn->setEnabled(x);
	}

IMMDevice * SoundPrison::determineDefaultDevice(EDataFlow flow)
	{
		IMMDevice * device;
		device_enum->GetDefaultAudioEndpoint(flow,
											 eMultimedia,
											 &device);
		return device;
	}

void SoundPrison::help()
	{
		QMessageBox::information(this,
								 tr("Sound Prison: Help"),
								 tr("<h3>About</h3>"
									"<p>"
									"<b>License:</b> GPL v3<br/>"
									"<b>Version:</b> 0.9"
									"</p>"
									"<p>"
									"Sound Prison is an audio control utility. "
									"Its purpose is to provide a way to manage and \"lock-up\" system-wide audio settings so that other programs don't \"get their way\" with <i>your</i> audio settings. "
									"This program is designed to work with Windows Vista or later; Windows XP and lower are not supported."
									"</p>"
									"<h3>Presets</h3>"
									"<p>"
									"Presets allow you to set parameters to be applied at a later time, either through the interface or with keyboard shortcuts. "
									"To edit a preset, select the preset you want to edit from the drop down menu and click <b>Edit</b>. "
									"The program will temporarily assume the values of the preset until you are done editing, "
									"then it'll apply the previous settings. Don't forget to save your preset settings after modifying them."
									"</p>"
									"<p>"
									"Sound Prison can be made to apply a predefined preset upon start up. "
									"To do this, simply check the check box <b>Apply on Start-up</b> when editing."
									"</p>"
									"<h3>Devices</h3>"
									"<p>"
									"Output devices are devices that generate sound. "
									"Input devices are devices used for recording sound. "
									"The default device for each category will be shown highlighted. "
									"You can switch between input devices by using the radio buttons next to each input device. "
									"This will make the device for that category the default device."
									"</p>"
									"<p>"
									"Sound Prison will list whatever devices are considered available by the operating system. "
									"Sound Prison can be set to prevent the system from adjusting the volume settings for each device. "
									"To do this, simply check the check box <b>Lock</b> for the desired device. "
									"Now whenever another program tries to adjust this device's volume settings, "
									"Sound Prison will pick up on it and reset the volume settings to what you have it set on Sound Prison. "
									"No more will another program have the power to dictate what it <i>thinks</i> you want."
									"</p>"));
	}

void SoundPrison::init()
	{
		IMMDevice * default_device;
		UINT device_count;
		HRESULT hr;
		QString defdev_name;

		hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&device_enum));
		if (FAILED(hr))
		{
			qDebug("Failed to instantiate Device Enumerator.");
			return;
		}

		device_factory->setEnumerator(device_enum);

		hr = device_enum->EnumAudioEndpoints(eRender,
											 DEVICE_STATE_ACTIVE | DEVICE_STATE_UNPLUGGED,
											 &device_collection);
		if (FAILED(hr))
			qDebug("Failed to retrieve Output Devices.");
		else
		{
			hr = device_collection->GetCount(&device_count);

			// Get the default output device
			default_device = determineDefaultDevice(eRender);
			if (default_device == NULL)
				qDebug("Failed to determine default device for output devices.");
			else
				defdev_name = device_factory->getDeviceName(default_device);

			// Loop through the output devices
			for (UINT i = 0; i < device_count; i += 1)
			{
				IMMDevice * device = 0;

				hr = device_collection->Item(i, &device);
				if (FAILED(hr))
					continue;

				QString device_name = device_factory->getDeviceName(device);

				DeviceWidget * output_device = new DeviceWidget(this);
				output_device->setDevice(device);
				output_device->setDeviceName(device_name);
				output_device->setDeviceType(eRender);

				connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
						output_device, SLOT(setEditMode(bool)), Qt::DirectConnection);

				connect(ui->lock_default_device_box, SIGNAL(toggled(bool)),
						output_device->radioBtn(), SLOT(setDisabled(bool)));

				if (default_device != NULL)
					output_device->radioBtn()->setChecked((defdev_name == output_device->deviceName()));

				if (output_device->radioBtn()->isChecked())
					default_output_device = output_device;

//				((QVBoxLayout *)ui->group_output_devices->layout())->addLayout(output_device->parentLayout());
				((QVBoxLayout *)ui->output_devices_scrollarea->widget()->layout())->addLayout(output_device->parentLayout());
				devices.append(output_device);
			}

			// Release the default device
			SAFE_RELEASE(default_device)
		}

		hr = device_enum->EnumAudioEndpoints(eCapture,
											 DEVICE_STATE_ACTIVE | DEVICE_STATE_UNPLUGGED,
											 &device_collection);
		if (FAILED(hr))
			qDebug("Failed to retrieve Input Devices.");
		else
		{
			hr = device_collection->GetCount(&device_count);

			// Get the default input device
			default_device = determineDefaultDevice(eCapture);
			if (default_device == NULL)
				qDebug("Failed to determine default device for output devices.");
			else
				defdev_name = device_factory->getDeviceName(default_device);

			// Loop through the input devices
			for (UINT i = 0; i < device_count; i += 1)
			{
				IMMDevice * device = 0;

				hr = device_collection->Item(i, &device);
				if (FAILED(hr))
					continue;

				QString device_name = device_factory->getDeviceName(device);

				DeviceWidget * input_device = new DeviceWidget(this);
				input_device->setDevice(device);
				input_device->setDeviceName(device_name);
				input_device->setDeviceType(eCapture);

				connect(ui->edit_preset_btn, SIGNAL(toggled(bool)),
						input_device, SLOT(setEditMode(bool)), Qt::DirectConnection);

				connect(ui->lock_default_device_box_2, SIGNAL(toggled(bool)),
						input_device->radioBtn(), SLOT(setDisabled(bool)));

				if (default_device != NULL)
					input_device->radioBtn()->setChecked((defdev_name == input_device->deviceName()));

				if (input_device->radioBtn()->isChecked())
					default_input_device = input_device;

//				((QVBoxLayout *)ui->group_input_devices->layout())->addLayout(input_device->parentLayout());
				((QVBoxLayout *)ui->input_devices_scrollarea->widget()->layout())->addLayout(input_device->parentLayout());
				devices.append(input_device);
			}

			// Release the default device
			SAFE_RELEASE(default_device)
		}
	}

void SoundPrison::loadPreset(bool l)
	{
		if (l)
		{
			foreach (DeviceWidget * dev_w, devices)
			{
				// Block until all devices are assuming edit mode.
				while (!dev_w->editMode())
					continue;
			}

			QString group = ui->presets_list->itemData(ui->presets_list->currentIndex()).toString();

			settings->beginGroup(group);
				if (settings->childKeys().count() < 2)
				{
					settings->endGroup();
					return;
				}

				ui->preset_shortcut_edit->setText(settings->value("shortcut").toString());
				ui->lock_default_device_box->setChecked(settings->value("output_devices_locked").toBool());
				ui->lock_default_device_box_2->setChecked(settings->value("input_devices_locked").toBool());
				ui->apply_startup_box->setChecked(settings->value("apply_on_start_up").toBool());
			settings->endGroup();

			settings->beginReadArray(group);
			for (int i = 0; i < devices.size(); ++i)
			{
				settings->setArrayIndex(i);
				QStringList list = settings->value("device").toStringList();
				foreach (DeviceWidget * dev_w, devices)
				{
					if (dev_w->deviceName() != list[0])
						continue;

					dev_w->setLocked((bool)list[1].toInt());
					dev_w->setVolume(list[2].toInt());
					dev_w->muteBox()->setChecked((bool)list[3].toInt());
					dev_w->radioBtn()->setChecked((bool)list[4].toInt());
				}
			}
			settings->endArray();
		}
	}

void SoundPrison::loadPresets()
	{
		QStringList groups = settings->childGroups();

		foreach (QString group, groups)
		{
			settings->beginGroup(group);
				ui->presets_list->addItem(settings->value("name", "Untitled").toString(),
										  group);
				QString shortcut = settings->value("shortcut").toString();
				if (!shortcut.isEmpty())
				{
					QxtGlobalShortcut * global_shortcut = new QxtGlobalShortcut(QKeySequence::fromString(shortcut), this);
					global_shortcuts.insert(group, global_shortcut);

					connect(global_shortcut, SIGNAL(activated()),
							this, SLOT(applyPresetByShortcut()));
				}

			settings->endGroup();
		}

		if (ui->presets_list->count())
		{
			ui->apply_preset_btn->setEnabled(true);
			ui->delete_preset_btn->setEnabled(true);
			ui->edit_preset_btn->setEnabled(true);
		}
	}

void SoundPrison::newPreset()
	{
		QDateTime datetime = QDateTime::currentDateTimeUtc();
		QByteArray hash = QCryptographicHash::hash(datetime.toString("MMddyyyyhhmmsszzz").toAscii(),
												   QCryptographicHash::Md5);
		QString hash_str;

		for (int i = 0; i < hash.size(); ++i)
		{
			unsigned char c = hash[i];
			c >>= 4;

			if (c < 10)
				c += '0';
			else
				c += 'A' - 10;

			hash_str += (char)c;
			c = hash[i];
			c &= 15;

			if (c < 10)
				c += '0';
			else
				c += 'A' - 10;

			hash_str += (char)c;
		}

		settings->beginWriteArray(hash_str, devices.size());
		settings->endArray();

		ui->presets_list->addItem("Untitled", hash_str);
		ui->preset_name_edit->setText("Untitled");
		ui->presets_list->setCurrentIndex(ui->presets_list->count()-1);

		ui->edit_preset_btn->setEnabled(true);
		ui->edit_preset_btn->setChecked(true);
	}

void SoundPrison::removeDevice(const QString & device_name)
	{
		foreach (DeviceWidget * out, devices)
		{
			if (out->deviceName() == device_name)
			{
				devices.takeAt(devices.indexOf(out))->deleteLater();
				break;
			}
		}
	}

void SoundPrison::savePreset()
	{
		QString group = ui->presets_list->itemData(ui->presets_list->currentIndex()).toString();

		ui->presets_list->setItemText(ui->presets_list->currentIndex(),
									  ui->preset_name_edit->text());

		settings->beginGroup(group);
			settings->setValue("apply_on_start_up",
							   ui->apply_startup_box->isChecked());
			settings->setValue("name",
							   ui->preset_name_edit->text());
			settings->setValue("output_devices_locked",
							   ui->lock_default_device_box->isChecked());
			settings->setValue("input_devices_locked",
							   ui->lock_default_device_box_2->isChecked());
			QString shortcut = ui->preset_shortcut_edit->text();
			settings->setValue("shortcut", shortcut);

			if (!shortcut.isEmpty())
			{
				if (global_shortcuts.contains(group))
				{
					if (global_shortcuts[group]->shortcut().toString()
					 != shortcut)
						global_shortcuts[group]->setShortcut(QKeySequence::fromString(shortcut));
				}

				else
				{
					QxtGlobalShortcut * global_shortcut = new QxtGlobalShortcut(QKeySequence::fromString(shortcut), this);
					global_shortcuts.insert(group, global_shortcut);
					connect(global_shortcut, SIGNAL(activated()),
							this, SLOT(applyPresetByShortcut()));
				}
			}
		settings->endGroup();

		settings->beginWriteArray(group, devices.size());
			for (int i = 0; i < devices.size(); ++i)
			{
				settings->setArrayIndex(i);
				settings->setValue("device", devices.at(i)->genSettings());
			}
		settings->endArray();
	}

void SoundPrison::setEditMode(bool e)
	{
		if (e)
		{
			edit_mode = e;

			input_devices_locked = ui->lock_default_device_box_2->isChecked();
			output_devices_locked = ui->lock_default_device_box->isChecked();
		}

		else
		{
			ui->lock_default_device_box->setChecked(output_devices_locked);
			ui->lock_default_device_box_2->setChecked(input_devices_locked);

			if (default_input_device)
				default_input_device->radioBtn()->setChecked(true);

			if (default_output_device)
				default_output_device->radioBtn()->setChecked(true);

			edit_mode = e;
		}
	}

void SoundPrison::systrayActivated(QSystemTrayIcon::ActivationReason r)
{
	if (r == QSystemTrayIcon::Trigger)
	{
		if (this->isHidden())
			show();
		else
			hide();
	}
}

void SoundPrison::updateDefaultDevice(const QString & dev_name,
									  const QString & dev_type)
	{
		DeviceWidget * prev_dev;
		QString device_type;

		// Determine previous default device
		foreach (DeviceWidget * dev_widget, devices)
		{
			if (dev_widget->deviceType() == eCapture)
				device_type = "eCapture";
			else if (dev_widget->deviceType() == eRender)
				device_type = "eRender";

			if (dev_type != device_type)
				continue;

			if (dev_name == dev_widget->deviceName())
				continue;

			if (dev_widget->radioBtn()->isChecked())
			{
				prev_dev = dev_widget;
				break;
			}
		}

		// If device is an input device and input devices are locked,
		//	set the previous device as current.
		if (dev_type == "eCapture"
		 && device_type == "eCapture"
		 && ui->lock_default_device_box_2->isChecked())
		{
			prev_dev->setAsDefaultDevice();
			default_input_device = prev_dev;
		}

		// If device is an output device and output devices are locked,
		//	set the previous device as current.
		else if (dev_type == "eRender"
		 && device_type == "eRender"
		 && ui->lock_default_device_box->isChecked())
		{
			prev_dev->setAsDefaultDevice();
			default_output_device = prev_dev;
		}

		else
		{
			foreach (DeviceWidget * dev_widget, devices)
			{
				if (dev_widget->deviceName() == dev_name)
				{
					if (!edit_mode)
						dev_widget->radioBtn()->setChecked(true);

					if (dev_widget->deviceType() == eCapture)
						default_input_device = dev_widget;
					else if (dev_widget->deviceType() == eRender)
						default_output_device = dev_widget;

					break;
				}
			}
		}
	}
